import { Fetcher, Route, Token } from '@uniswap/sdk';
import { Configuration } from './config';
import { ContractName, TokenStat, TreasuryAllocationTime } from './types';
import { BigNumber, Contract, ethers, Overrides } from 'ethers';
import { decimalToBalance } from './ether-utils';
import { TransactionResponse } from '@ethersproject/providers';
import ERC20 from './ERC20';
import { getDisplayBalance } from '../utils/formatBalance';
import { getDefaultProvider } from '../utils/provider';
import IUniswapV2PairABI from './IUniswapV2Pair.abi.json';
import { parseUnits } from 'ethers/lib/utils';
/**
 * An API module of Super Nova contracts.
 * All contract-interacting domain logic should be defined in here.
 */
export class BasisCash {
  myAccount: string;
  provider: ethers.providers.Web3Provider;
  signer?: ethers.Signer;
  config: Configuration;
  contracts: { [name: string]: Contract };
  externalTokens: { [name: string]: ERC20 };
  boardroomVersionOfUser?: string;

  bacDai: Contract;
  linshiBJ: ERC20;
  sCASH: ERC20;
  SHARE: ERC20;
  SNB: ERC20;
  LP: ERC20;
  LP1: ERC20;
  sFUND_USDT: ERC20;
  sFUND_SCASH: ERC20;
  sCASH_USDT_LP: ERC20;
  sHT: ERC20;

  constructor(cfg: Configuration) {
    const { deployments, externalTokens } = cfg;
    const provider = getDefaultProvider();

    // loads contracts from deployments
    this.contracts = {};
    for (const [name, deployment] of Object.entries(deployments)) {
      this.contracts[name] = new Contract(deployment.address, deployment.abi, provider);
    }
    this.externalTokens = {};
    for (const [symbol, [address, decimal]] of Object.entries(externalTokens)) {
      this.externalTokens[symbol] = new ERC20(address, provider, symbol, decimal); // TODO: add decimal
    }

    console.log('deployments',deployments)
    this.sCASH = new ERC20(deployments.sCASH.address, provider, 'sCASH');
    this.SHARE = new ERC20(deployments.SHARE.address, provider, 'SHARE');
    this.SNB = new ERC20(deployments.SHARE.address, provider, 'SNB');
    this.LP = new ERC20(externalTokens.sCASH_SHARE_LP[0], provider, 'LP');

    this.sFUND_USDT = new ERC20(externalTokens.USDT[0], provider, 'sFUND_USDT');
    this.sFUND_SCASH = new ERC20(externalTokens.sCASH[0], provider, 'sFUND_SCASH');
    this.sCASH_USDT_LP = new ERC20(externalTokens.sCASH_USDT_LP[0], provider, 'sCASH_USDT_LP');

    this.sHT = new ERC20(externalTokens.wHT[0], provider, 'sHT');

    // Uniswap V2 Pair
    this.bacDai = new Contract(
      externalTokens['sCASH_USDT_LP'][0],
      IUniswapV2PairABI,
      provider,
    );

    this.config = cfg;
    this.provider = provider;
  }

  /**
   * @param provider From an unlocked wallet. (e.g. Metamask)
   * @param account An address of unlocked wallet account.
   */
  unlockWallet(provider: any, account: string) {
    const newProvider = new ethers.providers.Web3Provider(provider, this.config.chainId);

    this.signer = newProvider.getSigner(0);
    this.myAccount = account;
    for (const [name, contract] of Object.entries(this.contracts)) {
      this.contracts[name] = contract.connect(this.signer);
    }
    const tokens = [this.sCASH, this.SHARE, this.SNB, this.LP, ...Object.values(this.externalTokens)];
    for (const token of tokens) {
      token.connect(this.signer);
    }
    this.bacDai = this.bacDai.connect(this.signer);
    console.log(`🔓 Wallet is unlocked. Welcome, ${account}!`);
    this.fetchBoardroomVersionOfUser()
      .then((version) => (this.boardroomVersionOfUser = version))
      .catch((err) => {
        console.error(`Failed to fetch boardroom version: ${err.stack}`);
        this.boardroomVersionOfUser = 'latest';
      });
  }

  get isUnlocked(): boolean {
    return !!this.myAccount;
  }

  gasOptions(gas: BigNumber): Overrides {
    const multiplied = Math.floor(gas.toNumber() * this.config.gasLimitMultiplier);
    console.log(`⛽️ Gas multiplied: ${gas} -> ${multiplied}`);
    return {
      gasLimit: BigNumber.from(multiplied),
    };
  }

  /**
   * @returns Super Nova (sCASH) stats from Uniswap.
   * It may differ from the sCASH price used on Treasury_sCASH (which is calculated in TWAP)
   */
  async getCashStatFromUniswap(): Promise<TokenStat> {
    const supply = await this.sCASH.displayedTotalSupply();
    return {
      priceInDAI: await this.getTokenPriceFromUniswap(this.sCASH),
      totalSupply: supply,
    };
  }

  async getCashStatFromUniswapSHT(): Promise<TokenStat> {
    const supply = await this.sHT.displayedTotalSupply();
    return {
      priceInDAI: await this.getTokenPriceFromUniswap(this.sHT),
      totalSupply: supply,
    };
  }

  // sFund
  async getsFundUsdtFunc(): Promise<TokenStat> {
    const supply = await this.sFUND_USDT.displayedBalanceOf(this.config.externalTokens.Fund[0]);
    const supply1 = await this.sFUND_SCASH.displayedBalanceOf(this.config.externalTokens.Fund[0]);
    return {
      priceInDAI: String(Number(supply).toFixed(4)),
      totalSupply: String(Number(supply1).toFixed(4)), 
    };
  }

  /**
   * @returns Estimated Super Nova (sCASH) price data,
   * calculated by 1-day Time-Weight Averaged Price (TWAP).
   */
  // async getCashStatInEstimatedTWAP(): Promise<TokenStat> {
  //   const { Oracle } = this.contracts;

  //   // estimate current TWAP price
  //   const cumulativePrice: BigNumber = await this.bacDai.price0CumulativeLast();
  //   const cumulativePriceLast = await Oracle.price0CumulativeLast();
  //   const elapsedSec = Math.floor(Date.now() / 1000 - (await Oracle.blockTimestampLast()));

  //   const denominator112 = BigNumber.from(2).pow(112);
  //   const denominator1e18 = BigNumber.from(10).pow(18);
  //   const cashPriceTWAP = cumulativePrice
  //     .sub(cumulativePriceLast)
  //     .mul(denominator1e18)
  //     .div(elapsedSec)
  //     .div(denominator112);

  //   const totalSupply = await this.sCASH.displayedTotalSupply();
  //   return {
  //     priceInDAI: getDisplayBalance(cashPriceTWAP),
  //     totalSupply,
  //   };
  // }


  async getCashStatInEstimatedTWAP(): Promise<TokenStat> {
    // const { SeigniorageOracle } = this.contracts;

    const expectedPrice = await this.contracts.sCASH_USDT_oracle.expectedPrice(
      this.sCASH.address,
      ethers.utils.parseEther('1'),
    );
    const supply = await this.sCASH.displayedTotalSupply();

    return {
      priceInDAI: getDisplayBalance(expectedPrice),
      totalSupply: supply,
    };
  }

  async getCashPriceInLastTWAP(): Promise<BigNumber> {
    const { Treasury_sCASH } = this.contracts;
    return Treasury_sCASH.getSeigniorageOraclePrice();
  }

  async getBondOraclePriceInLastTWAP(): Promise<BigNumber> {
    const { Treasury_sCASH } = this.contracts;
    return Treasury_sCASH.getBondOraclePrice();
  }

  async getBondStat(): Promise<TokenStat> {
    const decimals = BigNumber.from(10).pow(18);

    const cashPrice: BigNumber = await this.getBondOraclePriceInLastTWAP();
    const bondPrice = cashPrice.pow(2).div(decimals);

    return {
      priceInDAI: getDisplayBalance(bondPrice),
      totalSupply: await this.SNB.displayedTotalSupply(),
    };
  }

  async getShareStat(): Promise<TokenStat> {
    return {
      priceInDAI: await this.getTokenPriceFromUniswap(this.SHARE),
      totalSupply: await this.SHARE.displayedTotalSupply(),
    };
  }

  async getTokenPriceFromUniswap(tokenContract: ERC20): Promise<string> {
    await this.provider.ready;

    const { chainId } = this.config;
    const { HUSD } = this.config.externalTokens;

    const dai = new Token(chainId, HUSD[0], 8);
    const token = new Token(chainId, tokenContract.address, tokenContract.decimal);
    try {
      const daiToToken = await Fetcher.fetchPairData(dai, token, this.provider);
      const priceInDAI = new Route([daiToToken], token);
      return priceInDAI.midPrice.toSignificant(3);
    } catch (err) {
      console.error(`Failed to fetch token price of ${tokenContract.symbol}: ${err}`);
    }
  }

  /**
   * Buy bonds with cash.
   * @param amount amount of cash to purchase bonds with.
   */
  async buyBonds(amount: string | number): Promise<TransactionResponse> {
    const { Treasury_sCASH } = this.contracts;
    return await Treasury_sCASH.buyBonds(decimalToBalance(amount));
  }

  /**
   * Redeem bonds for cash.
   * @param amount amount of bonds to redeem.
   */
  async redeemBonds(amount: string): Promise<TransactionResponse> {
    const { Treasury_sCASH } = this.contracts;
    return await Treasury_sCASH.redeemBonds(decimalToBalance(amount));
  }

  async earnedFromBank(poolName: ContractName, account = this.myAccount): Promise<BigNumber> {
    const pool = this.contracts[poolName];
    try {
      return await pool.earned(account);
    } catch (err) {
      console.error(`Failed to call earned() on pool ${pool.address}: ${err.stack}`);
      return BigNumber.from(0);
    }
  }

  async stakedBalanceOnBank(
    poolName: ContractName,
    account = this.myAccount,
  ): Promise<BigNumber> {
    const pool = this.contracts[poolName];
    try {
      return await pool.balanceOf(account);
    } catch (err) {
      console.error(`Failed to call balanceOf() on pool ${pool.address}: ${err.stack}`);
      return BigNumber.from(0);
    }
  }

  /**
   * Deposits token to given pool.
   * @param poolName A name of pool contract.
   * @param amount Number of tokens with decimals applied. (e.g. 1.45 HUSD * 10^18)
   * @returns {string} Transaction hash
   */
  async stake(poolName: ContractName, amount: BigNumber): Promise<TransactionResponse> {
    const pool = this.contracts[poolName];
    const gas = await pool.estimateGas.stake(amount);
    return await pool.stake(amount, this.gasOptions(gas));
  }

  /**
   * Withdraws token from given pool.
   * @param poolName A name of pool contract.
   * @param amount Number of tokens with decimals applied. (e.g. 1.45 HUSD * 10^18)
   * @returns {string} Transaction hash
   */
  async unstake(poolName: ContractName, amount: BigNumber): Promise<TransactionResponse> {
    const pool = this.contracts[poolName];
    const gas = await pool.estimateGas.withdraw(amount);
    return await pool.withdraw(amount, this.gasOptions(gas));
  }

  /**
   * Transfers earned token reward from given pool to my account.
   */
  async harvest(poolName: ContractName): Promise<TransactionResponse> {
    const pool = this.contracts[poolName];
    const gas = await pool.estimateGas.getReward();
    return await pool.getReward(this.gasOptions(gas));
  }

  /**
   * Harvests and withdraws deposited tokens from the pool.
   */
  async exit(poolName: ContractName): Promise<TransactionResponse> {
    const pool = this.contracts[poolName];
    const gas = await pool.estimateGas.exit();
    return await pool.exit(this.gasOptions(gas));
  }

  async fetchBoardroomVersionOfUser(): Promise<string> {
    // const { Boardroom1, Boardroom2 } = this.contracts;
    // const balance1 = await Boardroom1.getShareOf(this.myAccount);
    // if (balance1.gt(0)) {
    //   console.log(
    //     `👀 The user is using Boardroom v1. (Staked ${getDisplayBalance(balance1)} SHARE)`,
    //   );
    //   return 'v1';
    // }
    // const balance2 = await Boardroom2.balanceOf(this.myAccount);
    // if (balance2.gt(0)) {
    //   console.log(
    //     `👀 The user is using Boardroom v2. (Staked ${getDisplayBalance(balance2)} SHARE)`,
    //   );
    //   return 'v2';
    // }
    return 'latest';
  }

  boardroomByVersion(version: string): Contract {
    if (version === 'v1') {
      return this.contracts.LPBoardroom_sCASH;
    }
    if (version === 'v2') {
      return this.contracts.LPBoardroom_sCASH;
    }
    return this.contracts.LPBoardroom_sCASH;
  }

  boardroomByVersion1(version: string): Contract {
    if (version === 'v1') {
      return this.contracts.ShareBoardroom_sCASH;
    }
    if (version === 'v2') {
      return this.contracts.ShareBoardroom_sCASH;
    }
    return this.contracts.ShareBoardroom_sCASH;
  }

  currentBoardroom(): Contract {
    if (!this.boardroomVersionOfUser) {
      throw new Error('you must unlock the wallet to continue.');
    }
    return this.boardroomByVersion(this.boardroomVersionOfUser);
  }

  currentBoardroom1(): Contract {
    if (!this.boardroomVersionOfUser) {
      throw new Error('you must unlock the wallet to continue.');
    }
    return this.boardroomByVersion1(this.boardroomVersionOfUser);
  }

  isOldBoardroomMember(): boolean {
    return this.boardroomVersionOfUser !== 'latest';
  }

  async stakeShareToBoardroom(amount: string): Promise<TransactionResponse> {
    if (this.isOldBoardroomMember()) {
      throw new Error("you're using old Boardroom. please withdraw and deposit the SHARE again.");
    }
    const Boardroom = this.currentBoardroom();
    return await Boardroom.stake(decimalToBalance(amount));
  }

  async getStakedSharesOnBoardroom(): Promise<BigNumber> {
    const Boardroom = this.currentBoardroom();
    if (this.boardroomVersionOfUser === 'v1') {
      return await Boardroom.getShareOf(this.myAccount);
    }
    return await Boardroom.balanceOf(this.myAccount);
  }

  async getEarningsOnBoardroom(): Promise<BigNumber> {
    const Boardroom = this.currentBoardroom();
    if (this.boardroomVersionOfUser === 'v1') {
      return await Boardroom.getCashEarningsOf(this.myAccount);
    }
    return await Boardroom.earned(this.myAccount);
  }

  async withdrawShareFromBoardroom(amount: string): Promise<TransactionResponse> {
    const Boardroom = this.currentBoardroom();
    return await Boardroom.withdraw(decimalToBalance(amount));
  }

  async harvestCashFromBoardroom(): Promise<TransactionResponse> {
    const Boardroom = this.currentBoardroom();
    if (this.boardroomVersionOfUser === 'v1') {
      return await Boardroom.claimDividends();
    }
    return await Boardroom.claimReward();
  }

  async exitFromBoardroom(): Promise<TransactionResponse> {
    const Boardroom = this.currentBoardroom();
    return await Boardroom.exit();
  }

  // Boardroom 复制版本

  async stakeShareToBoardroom1(amount: string): Promise<TransactionResponse> {
    if (this.isOldBoardroomMember()) {
      throw new Error("you're using old Boardroom. please withdraw and deposit the SHARE again.");
    }
    const Boardroom = this.currentBoardroom1();
    return await Boardroom.stake(decimalToBalance(amount));
  }

  async getStakedSharesOnBoardroom1(): Promise<BigNumber> {
    const Boardroom = this.currentBoardroom1();
    if (this.boardroomVersionOfUser === 'v1') {
      return await Boardroom.getShareOf(this.myAccount);
    }
    return await Boardroom.balanceOf(this.myAccount);
  }

  async getEarningsOnBoardroom1(): Promise<BigNumber> {
    const Boardroom = this.currentBoardroom1();
    if (this.boardroomVersionOfUser === 'v1') {
      return await Boardroom.getCashEarningsOf(this.myAccount);
    }
    return await Boardroom.earned(this.myAccount);
  }

  async withdrawShareFromBoardroom1(amount: string): Promise<TransactionResponse> {
    const Boardroom = this.currentBoardroom1();
    return await Boardroom.withdraw(decimalToBalance(amount));
  }

  async harvestCashFromBoardroom1(): Promise<TransactionResponse> {
    const Boardroom = this.currentBoardroom1();
    if (this.boardroomVersionOfUser === 'v1') {
      return await Boardroom.claimDividends();
    }
    return await Boardroom.claimReward();
  }

  async exitFromBoardroom1(): Promise<TransactionResponse> {
    const Boardroom = this.currentBoardroom1();
    return await Boardroom.exit();
  }

  // 结束Boardroom

  async getTreasuryNextAllocationTime(): Promise<TreasuryAllocationTime> {
    const { Treasury_sCASH } = this.contracts;
    const nextEpochTimestamp: BigNumber = await Treasury_sCASH.nextEpochPoint();
    const period: BigNumber = await Treasury_sCASH.getPeriod();

    const nextAllocation = new Date(nextEpochTimestamp.mul(1000).toNumber());
    const prevAllocation = new Date(nextAllocation.getTime() - period.toNumber() * 1000);
    
    
    return { prevAllocation, nextAllocation };
  }

  async getTVL(pool: string, token: ERC20): Promise<string> {
    let configs: {[key: string]: number} = {
      'USDT': 1, 
      'HUSD': 1, 
      'HBTC': 49363, 
      'wETH': 1642, 
      'wHT': 19.37, 
      'pNEO': 41.74, 
      'HFIL': 36.94, 
      'HDOT': 34.63,
      'HLTC': 181.41,
      'BAG': 1,

      'fHUSD': 0.025, 
      'fUSDT': 0.025, 
      'fHBTC': 49363 / 40, 
      'fETH': 1642 / 40, 
      'fHT': 19.37 / 40, 
      'fNEO': 41.74 / 40, 
      'fHFIL': 36.94 / 40,
      'fHDOT': 34.63 / 40,
      'fHLTC': 181.41 / 40,
      'Filda': 1.7,
      'TPT': 0.05
    };

    // this.linshiBJ = new ERC20(this.config.externalTokens[token.symbol][0], this.provider, token.symbol);

    // try{
      let midPrice: number = configs[token.symbol];
      let midPrices: string;
      // if (token.symbol == 'USDT' || token.symbol == 'HUSD') {
      //   midPrices = '1';
      // } else {
      //   midPrices = await this.getTokenPriceFromUniswap(token);
      // }

      let ymidPrices: number;
      if (midPrices) {
        ymidPrices = Number(midPrices);
      } else {
        ymidPrices = Number(midPrice);
      } 

      const totalSupply: BigNumber = await this.contracts[pool].totalSupply();
      console.log('externalTokens', token.symbol, midPrice, midPrices, '++++',  ymidPrices);
      
      if(ymidPrices && totalSupply){
        return ethers.utils.formatUnits(BigNumber.from(parseInt((ymidPrices * 10000).toString())).mul(totalSupply).div(10000), token.decimal);
      }
      return BigNumber.from(0).toString(); 
    // }catch (err) {
    //   return BigNumber.from(0).toString();
    // }
  }

  async getAPY(pool: string, token: ERC20): Promise<string> {
    // try{
      //const reward = await this.contracts[pool].rewardRate();
      const reward = 100;
      const scashPrice: string = await this.getTokenPriceFromUniswap(this.sCASH);
    
      const tvl = await this.getTVL(pool, token);
      if(BigNumber.from(parseInt(tvl)) <= BigNumber.from(0)){
        return '0';
      }
      // let apy = reward.div(5).mul(BigNumber.from(scashPrice)).mul(365).div(tvl);
      let apy = reward / 5 * parseFloat(scashPrice) * 365 / parseFloat(tvl);
      return apy.toString();

    // }catch (err) {
    //   return BigNumber.from(0).toString();
    // }
  }

  async getTVL2(pool: string): Promise<string> {
    // console.log(pool,"pool========");
    
    let name = 'USDT';
    let count = 18;
    if(pool == '0x29471983b759Ccb802f486244B26A7c7dce8b7B5'){
      name = 'HUSD';
      count = 8;
    }
    if(pool == '0xb1188f0bb3eb9a49C0bfcFA87238773352C151C4'){
      name = 'HUSD';
      count = 8;
    }
   
    const usdt = this.externalTokens[name];
    let total: BigNumber = await usdt.balanceOf(pool);
    return ethers.utils.formatUnits(total.mul(2).toString(), count);
  }

  async getAPY2(pool: string): Promise<string> {
    let name = 'sCASH';
    let reward = 100 / 3;
   
    if(pool == '0xb1188f0bb3eb9a49C0bfcFA87238773352C151C4' || pool == '0xF61e00443FF42371b9efC4434a610F6e7C0C84aA'){
      name = 'SHARE';
      reward = 25 / 3;
    }
      
      const sharePrice: string = await this.getTokenPriceFromUniswap(this.SHARE);
    
      const tvl = await this.getTVL2(pool);
      if(parseFloat(tvl) <= 0){
        return '0';
      }
      // let apy = reward.div(5).mul(BigNumber.from(scashPrice)).mul(365).div(tvl);
      let apy = reward * parseFloat(sharePrice) * 365 / parseFloat(tvl);
      return apy.toString();

    // }catch (err) {
    //   return BigNumber.from(0).toString();
    // }
  }


}
